﻿using System;
using System.Diagnostics;
using System.Globalization;
using System.Xml.Serialization;

namespace WGS84
{
    /// <summary>
    /// Representation of location coordinates in WGS84 Latitude/Longitude
    /// </summary>    
    [Serializable()]
    [XmlRoot(Namespace = "http://www.anotherurl.com/LightFramework")]
    public class Wgs84Coordinate
    {
        /// <summary>
        /// Copy Constructor
        /// </summary>
        /// <param name="coordinate">Wgs84 Coordinate</param>
        [DebuggerHidden]
        public Wgs84Coordinate(Wgs84Coordinate coordinate)
        {
            if (coordinate == null)
            {
                throw new ArgumentNullException("coordinate");
            }
            _lat = coordinate.Latitude;
            _long = coordinate.Longitude;
        }

        /// <summary>
        /// Assign an existing Lat/Long coordinate to this object
        /// </summary>
        /// <param name="latitude"></param>
        /// <param name="longitude"></param>
        /// 

        [DebuggerStepThrough]
        public Wgs84Coordinate(float latitude, float longitude)
        {
            _lat = latitude;
            _long = longitude;
        }

        /// <summary>
        /// Assign an existing Lat/Long coordinate to this object
        /// </summary>
        /// <param name="latitude"></param>
        /// <param name="longitude"></param>
        [DebuggerStepThrough]
        public Wgs84Coordinate(double latitude, double longitude)
        {
            _lat = latitude;
            _long = longitude;
        }

        private Wgs84Coordinate()
        {
        }

        private double _long;

        /// <summary>
        /// Longitude
        /// </summary>        
        [XmlElement(ElementName = "longitude")]
        public double Longitude
        {
            get { return _long; }
            set { _long = value; }
        }

        private double _lat;

        /// <summary>
        /// Latitude
        /// </summary>
        [XmlElement(ElementName = "latitude")]
        public double Latitude
        {
            get { return _lat; }
            set { _lat = value; }
        }

        /// <summary>
        /// Performs value comparison of fields for this coordinate
        /// </summary>
        /// <param name="obj">Coordinate to compare</param>
        /// <returns>Whether two coordinates are equal</returns>
        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }

            // Don't do this or derived types will fail
            if (GetType() != obj.GetType())
            {
                return false;
            }

            Wgs84Coordinate coordinate = obj as Wgs84Coordinate;
            if (coordinate != null)
            {
                if (Latitude.Equals(coordinate.Latitude) &&
                    Longitude.Equals(coordinate.Longitude)
                    )
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Calculate hash code based on member variables
        /// </summary>
        /// <returns>Hashcode</returns>
        public override int GetHashCode()
        {
            int code = Latitude.GetHashCode() ^ Longitude.GetHashCode();
            return code;
        }

        /// <summary>
        /// String representation of coordinate
        /// </summary>
        /// <returns>Legible string</returns>
        public override string ToString()
        {
            return string.Format(CultureInfo.CurrentCulture, "{0} {1}", Latitude, Longitude);
        }

        /// <summary>
        /// Calculate distance (in km) between two points specified by latitude/longitude.
        ///
        /// from: Haversine formula - R. W. Sinnott, "Virtues of the Haversine",
        ///       Sky and Telescope, vol 68, no 2, 1984
        ///       http://www.census.gov/cgi-bin/geo/gisfaq?Q5.1
        ///		  http://www.movable-type.co.uk/scripts/LatLong.html
        ///
        /// </summary>
        /// <returns>Distance in km</returns>
        public double Distance(Wgs84Coordinate other)
        {
            Degrees p1Lo = new Degrees(Longitude);
            Degrees p1La = new Degrees(Latitude);
            Degrees p2Lo = new Degrees(other.Longitude);
            Degrees p2La = new Degrees(other.Latitude);

            double p1Long = p1Lo.ToRadians();
            double p1Lat = p1La.ToRadians();
            double p2Long = p2Lo.ToRadians();
            double p2Lat = p2La.ToRadians();

            float R = 6371.0F; // earth's mean radius in km
            double dLat = p2Lat - p1Lat;
            double dLong = p2Long - p1Long;

            double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
                       Math.Cos(p1Lat) * Math.Cos(p2Lat) * Math.Sin(dLong / 2) * Math.Sin(dLong / 2);
            double c = 2.0F * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1.0 - a));
            double d = R * c;

            return d;
        }

        /// <summary>
        ///       
        /// calculate (initial) bearing (in degrees clockwise) between two points
        /// from: Ed Williams' Aviation Formulary, http://williams.best.vwh.net/avform.htm#Crs
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public Bearing Bearing(Wgs84Coordinate other)
        {
            Degrees p1Lo = new Degrees(Longitude);
            Degrees p1La = new Degrees(Latitude);
            Degrees p2Lo = new Degrees(other.Longitude);
            Degrees p2La = new Degrees(other.Latitude);

            double p1Long = p1Lo.ToRadians();
            double p1Lat = p1La.ToRadians();
            double p2Long = p2Lo.ToRadians();
            double p2Lat = p2La.ToRadians();

            double y = Math.Sin(p2Long - p1Long) * Math.Cos(p2Lat);
            double x = Math.Cos(p1Lat) * Math.Sin(p2Lat) -
                       Math.Sin(p1Lat) * Math.Cos(p2Lat) * Math.Cos(p2Long - p1Long);

            double radbearing = Math.Atan2(y, x); // -y 'cos Williams treats W as +ve!
            Bearing b = new Bearing(new Radians(Math.Abs(radbearing)));
            return b;
            //return Math.Abs(radbearing*180.0/Math.PI);
        }

        // see http://www.movable-type.co.uk/scripts/latlong.html

        /// <summary>
        /// Mid Point
        /// Just as the initial bearing may vary from the final bearing, the midpoint 
        /// may not be located half-way between latitudes/longitudes; the midpoint
        /// between 35°N,45°E and 35°N,135°E is around 45°N,90°E.
        /// </summary>
        /// <param name="other">The far point</param>
        /// <returns> the mid point</returns>
        public Wgs84Coordinate MidPoint(Wgs84Coordinate other)
        {
            Degrees p1Lo = new Degrees(Longitude);
            Degrees p1La = new Degrees(Latitude);
            Degrees p2Lo = new Degrees(other.Longitude);
            Degrees p2La = new Degrees(other.Latitude);

            double p1Long = p1Lo.ToRadians();
            double p1Lat = p1La.ToRadians();
            double p2Long = p2Lo.ToRadians();
            double p2Lat = p2La.ToRadians();

            double dLon = p2Long - p1Long;

            var Bx = Math.Cos(p2Lat) * Math.Cos(dLon);
            var By = Math.Cos(p2Lat) * Math.Sin(dLon);
            var lat3 = Math.Atan2(Math.Sin(p1Lat) + Math.Sin(p2Lat),
                                  Math.Sqrt((Math.Cos(p1Lat) + Bx) * (Math.Cos(p1Lat) + Bx) + By * By));
            var lon3 = p1Long + Math.Atan2(By, Math.Cos(p1Lat) + Bx);

            return new Wgs84Coordinate(lat3 * 180.0 / Math.PI, lon3 * 180.0 / Math.PI);
        }


        // see http://www.movable-type.co.uk/scripts/latlong.html

        /// <summary>
        ///       
        /// calculate (initial) bearing (in degrees clockwise) between two points
        /// from: Ed Williams' Aviation Formulary, http://williams.best.vwh.net/avform.htm#Crs
        /// </summary>
        /// <param name="p1"></param>
        /// <param name="p2"></param>
        /// <returns></returns>
        public static double BearingV1(Wgs84Coordinate p1, Wgs84Coordinate p2)
        {
            Degrees p1Lo = new Degrees(p1.Longitude);
            Degrees p1La = new Degrees(p1.Latitude);
            Degrees p2Lo = new Degrees(p2.Longitude);
            Degrees p2La = new Degrees(p2.Latitude);

            double p1Long = p1Lo.ToRadians();
            double p1Lat = p1La.ToRadians();
            double p2Long = p2Lo.ToRadians();
            double p2Lat = p2La.ToRadians();

            double y = Math.Sin(p2Long - p1Long) * Math.Cos(p2Lat);
            double x = Math.Cos(p1Lat) * Math.Sin(p2Lat) -
                       Math.Sin(p1Lat) * Math.Cos(p2Lat) * Math.Cos(p2Long - p1Long);

            double radbearing = Math.Atan2(y, x); // -y 'cos Williams treats W as +ve!

            double deg = radbearing * 180.0 / Math.PI;
            return (deg + 360.0) % 360.0;
        }
    }

}
